W84 — Incidents That Happen On Their Own - #360
Conversation
System 16 rolls declared-scope incidents (world, then zone id, then building id) against each eligible definition's chance and a weighted choice among successes, blocks a definition/scope already active or still in cooldown, and now also resolves on a true resolutionCondition, not only expiry. Allocated occurrences apply their onStart effects before system 17, and incident.raised joins the already-delivered incident.resolved. Registers incident_raised as an audit reason (it reaches a visible StateChange via onStart's EffectContext exactly like the existing indirect five), and adds a dormant "storm" definition to the MVP campaign — gated on every staff role being hired at once, which no existing fixture does — plus the new world-graph-mvp-incidents replay fixture proving the roll fires.
|
Two additional findings from the code review that can't be posted as inline comments because they land in files this PR doesn't touch:
|
Extends the undeferrable-wear-delta validation to incidents[].onStart, per the fork design/90-decisions.md left for W84 to resolve (onStart now only ever runs from system 16, after system 14 has closed its broken-transition check). Also removes a content-dependent RNG-draw skip in the weighted incident selection, collapses incidentBlocked's scope match to a plain equality check, extracts the duplicated IntegerRange-duration-collapse logic into a shared resolveDuration helper, and hoists the per-scope eligible-definitions computation out of the per-instance loop.
|
Update on the two findings above:
|
What changed, and why. System 16 (
incidents) previously only expired duration-boundoccurrences; it never rolled a new one, so litter was the only incident that could ever occur.
It now visits scopes in world → zone id → building id order, rolls each eligible definition
against its own declared chance from a single per-tick
tick:${tick}:incidentshandle,picks among successes by weight when more than one passes, and blocks a
definition/scope that is still active or inside its cooldown. It also resolves an occurrence
on a true
resolutionCondition, not only on expiry, writingresolvedAtTickbefore thatoccurrence's resolve effects run. A roll's
onStarteffects apply before system 17(
objectives), andincident.raisednow joins the already-deliveredincident.resolved.Registered
incident_raisedas an audit reason — like the existing "indirect five", it canreach a visible
StateChangevia anonStarteffect'sEffectContext.reason. Added adormant
stormincident to the MVP campaign, gated on every one of the three staff rolesbeing hired at once (no existing fixture hires all three), plus a new
world-graph-mvp-incidentsreplay fixture that exercises the roll for real.A decision the contract (20-contract.md §4.18) left open: it does not specify how a
scope's own roll behaves when more than one definition is eligible for it in the same tick.
I implemented "each eligible definition rolls its own chance independently against its own
rollChanceBasisPoints, then a weighted choice picks one occurrence among the successes" —this matches W84.1's "an eligible definition rolls against its declared chance" (per
definition, not per scope) while still reading as one "draw chance and then weighted choice"
per scope when (as in every fixture here) only one definition is ever eligible for a given
scope at once.
Closes #355
Verified
Not yet run — the gates run next and this section is replaced with their report.
Agent detail
design/30-slices.md§ W84 @6fd30e04106b4db4db7614faa0e474ff1b160405npm run typecheck,npm run lintandnpm testall pass locally(confirmed manually), but this is
/pr's gate run to record formally, not/slice's